import random, sys, copy, os, pygame
from pygame.locals import *

FPS = 30
WINWIDTH = 1600
WINHEIGHT =800
HALF_WINWIDTH = WINWIDTH // 2
HALF_WINHEIGHT = WINHEIGHT // 2

TILEWIDTH = 50
TILEHEIGHT = 85
TILEFLOORHEIGHT = 45

CAM_MOVE_SPEED = 5

OUTSIDE_DECORATION_PCT = 20

BRIGHTBLUE = (0, 170, 255)
WHITE = (255, 255, 255)
BGCOLOR = BRIGHTBLUE
TEXTCOLOR = WHITE

UP = '向上'
DOWN = '向下'
LEFT = '向左'
RIGHT = '向右'

def startScreen():
    titleRect = IMAGESDICT['title'].get_rect()
    topCoord = 100
    titleRect.top = topCoord
    titleRect.centerx = HALF_WINWIDTH
    topCoord += titleRect.height
    instructionText = ['把星星推到有星星阴影的位置',
                       '方向键移动星星，P键更换头像',
                       'Backspace退格键复位重来，Esc键退出',
                       'N键进入下一关，B键退回到上一关']
    DISPLAYSURF.fill(BGCOLOR)
    DISPLAYSURF.blit(IMAGESDICT['title'], titleRect)
    for i in range(len(instructionText)):
        instSurf = BASICFONT.render(instructionText[i], True, TEXTCOLOR)
        instRect = instSurf.get_rect()
        topCoord += 10
        instRect.top = topCoord
        instRect.centerx = HALF_WINWIDTH
        topCoord += instRect.height
        DISPLAYSURF.blit(instSurf, instRect)
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                terminate()
            elif event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    terminate()
                return
        pygame.display.update()
        FPSCLOCK.tick(FPS)
def readLevelsFile(filename):
    assert os.path.exists(filename), '未找到文件：%s' % (filename)
    
    with open(filename, 'r') as mapFile:
        content = mapFile.readlines() + ['\r\n']
    
    levels = []
    levelNum = 0
    mapTextLines = []
    mapObj = []
    for lineNum in range(len(content)):
        line = content[lineNum].rstrip('\r\n')
        if ';' in line:
            line = line[:line.find(';')]
        if line != '':
            mapTextLines.append(line)
        elif line == '' and len(mapTextLines) > 0:
            maxWidth = -1
            for i in range(len(mapTextLines)):
                if len(mapTextLines[i]) > maxWidth:
                    maxWidth = len(mapTextLines[i])
            for i in range(len(mapTextLines)):
                mapTextLines[i] += ' ' * (maxWidth - len(mapTextLines[i]))
            
            for x in range(len(mapTextLines[0])):
                mapObj.append([])
            for y in range(len(mapTextLines)):
                for x in range(maxWidth):
                    mapObj[x].append(mapTextLines[y][x])
            
            startx, starty = 0, 0
            goals, stars = [], []
            for x in range(maxWidth):
                for y in range(len(mapObj[x])):
                    if mapObj[x][y] in ('@', '+'):
                        startx, starty = x, y
                    if mapObj[x][y]in ('.', '+', '*'):
                        goals.append((x, y))
                    if mapObj[x][y] in ('$', '*'):
                        stars.append((x, y))
            
            assert startx != None and starty != None, '文件 %s 中谜题 %s (第 %s 行附近) 缺少一个"@"或"+"来标记起始点' % (filename, levelNum+1, lineNum)
            assert len(goals) > 0, '文件 %s 中谜题 %s (第 %s 行附近) 至少应有一个目标点' % (filename, levelNum+1, lineNum)
            assert len(stars) >= len(goals), '文件 %s 中谜题 %s (第 %s 行附近) 不可能解开，因为有 %s 个目标点， 却只有 %s 个星星' % (filename, levelNum+1, lineNum, len(goals), len(stars))
            gameStateObj = {'player': (startx, starty),
                            'stepCounter': 0,
                            'stars': stars}
            levelObj = {'width': maxWidth,
                        'height': len(mapObj),
                        'mapObj': mapObj,
                        'goals': goals,
                        'startState': gameStateObj}
            levels.append(levelObj)
            mapTextLines = []
            mapObj = []
            gameStateObj = {}
            levelNum += 1
    return levels
def decorateMap(mapObj, startxy):
    startx, starty = startxy
    mapObjCopy = copy.deepcopy(mapObj)
    for x in range(len(mapObjCopy)):
        for y in range(len(mapObjCopy[0])):
            if mapObjCopy[x][y] in ('$', '.', '@', '+', '*'):
                mapObjCopy[x][y] = ' '
    floodFill(mapObjCopy, startx, starty, ' ', 'o')
    for x in range(len(mapObjCopy)):
        for y in range(len(mapObjCopy[0])):
            if mapObjCopy[x][y] == '#':
                if (isWall(mapObjCopy, x, y-1) and isWall(mapObjCopy, x+1, y)) or \
                   (isWall(mapObjCopy, x+1, y) and isWall(mapObjCopy, x, y+1)) or \
                   (isWall(mapObjCopy, x, y+1) and isWall(mapObjCopy, x-1, y)) or \
                   (isWall(mapObjCopy, x-1, y) and isWall(mapObjCopy, x, y-1)):
                    mapObjCopy[x][y] = 'x'
            elif mapObjCopy[x][y] == ' ' and random.randint(0, 99) < OUTSIDE_DECORATION_PCT:
                mapObjCopy[x][y] = random.choice(list(OUTSIDEDECOMAPPING.keys()))
    return mapObjCopy
def floodFill(mapObj, x, y, oldCharacter, newCharacter):
    if mapObj[x][y] == oldCharacter:
        mapObj[x][y] = newCharacter
    if x < len(mapObj) - 1 and mapObj[x+1][y] == oldCharacter:
        floodFill(mapObj, x+1, y, oldCharacter, newCharacter)
    if x > 0 and mapObj[x-1][y] == oldCharacter:
        floodFill(mapObj, x-1, y, oldCharacter, newCharacter)
    if y < len(mapObj[x]) - 1 and mapObj[x][y+1] == oldCharacter:
        floodFill(mapObj, x, y+1, oldCharacter, newCharacter)
    if y > 0 and mapObj[x][y-1] == oldCharacter:
        floodFill(mapObj, x, y-1, oldCharacter, newCharacter)
def isWall(mapObj, x, y):
    if x < 0 or x >= len(mapObj) or y < 0 or y >= len(mapObj[x]):
        return False
    elif mapObj[x][y] in ('#', 'x'):
        return True
    return False
def drawMap(mapObj, gameStateObj, goals):
    mapSurfWidth = len(mapObj) * TILEWIDTH
    mapSurfHeight = (len(mapObj[0]) - 1) * (TILEHEIGHT - TILEFLOORHEIGHT) + TILEHEIGHT
    mapSurf = pygame.Surface((mapSurfWidth, mapSurfHeight))
    mapSurf.fill(BGCOLOR)
    for x in range(len(mapObj)):
        for y in range(len(mapObj[x])):
            spaceRect = pygame.Rect((x*TILEWIDTH, y*(TILEHEIGHT-TILEFLOORHEIGHT), TILEWIDTH, TILEHEIGHT))
            if mapObj[x][y] in TILEMAPPING:
                baseTile = TILEMAPPING[mapObj[x][y]]
            elif mapObj[x][y] in OUTSIDEDECOMAPPING:
                baseTile = TILEMAPPING[' ']
            mapSurf.blit(baseTile, spaceRect)
            
            if mapObj[x][y] in OUTSIDEDECOMAPPING:
                mapSurf.blit(OUTSIDEDECOMAPPING[mapObj[x][y]], spaceRect)
            elif (x, y) in gameStateObj['stars']:
                if (x, y) in goals:
                    mapSurf.blit(IMAGESDICT['covered goal'], spaceRect)
                mapSurf.blit(IMAGESDICT['star'], spaceRect)
            elif (x, y) in goals:
                mapSurf.blit(IMAGESDICT['covered goal'], spaceRect)
            
            if (x, y) == gameStateObj['player']:
                mapSurf.blit(PLAYERIMAGES[currentImage], spaceRect)
    return mapSurf
def runLevel(levels, levelNum):
    global currentImage
    levelObj = levels[levelNum]
    mapObj = decorateMap(levelObj['mapObj'], levelObj['startState']['player'])
    gameStateObj = copy.deepcopy(levelObj['startState'])
    mapNeedsRedraw = True
    
    levelSurf = BASICFONT.render('Level %s of %s' % (levelNum+1, len(levels)), True, TEXTCOLOR)
    levelRect = levelSurf.get_rect()
    levelRect.bottomleft = (20, WINHEIGHT - 35)
    
    levelIsComplete = False
    while True:
        playerMoveTo = None
        keyPressed = False
        for event in pygame.event.get():
            if event.type == QUIT:
                terminate()
            elif event.type == KEYDOWN:
                keyPressed = True
                if event.key == K_ESCAPE:
                    terminate()
                elif event.key == K_p:
                    currentImage += 1
                    currentImage %= len(PLAYERIMAGES)
                    mapNeedsRedraw = True
                elif event.key == K_n:
                    return 'next'
                elif event.key == K_b:
                    return 'back'
                elif event.key == K_BACKSPACE:
                    return 'reset'
                elif event.key == K_UP or event.key == K_w:
                    playerMoveTo = UP
                elif event.key == K_DOWN or event.key == K_s:
                    playerMoveTo = DOWN
                elif event.key == K_LEFT or event.key == K_a:
                    playerMoveTo = LEFT
                elif event.key == K_RIGHT or event.key == K_d:
                    playerMoveTo = RIGHT
        if playerMoveTo != None:
            moved = makeMove(mapObj, gameStateObj, playerMoveTo)
            if moved:
                mapNeedsRedraw = True
                gameStateObj['stepCounter'] += 1
            if isLevelFinished(levelObj, gameStateObj):
                levelIsComplete = True
                keyPressed = False
            
        DISPLAYSURF.fill(BGCOLOR)
        if mapNeedsRedraw:
            mapSurf = drawMap(mapObj, gameStateObj, levelObj['goals'])
            mapNeedsRedraw = False
        
        mapSurfRect = mapSurf.get_rect()
        mapSurfRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT)
        DISPLAYSURF.blit(mapSurf, mapSurfRect)
        DISPLAYSURF.blit(levelSurf, levelRect)
        stepSurf = BASICFONT.render('Steps: %s' % (gameStateObj['stepCounter']), True, TEXTCOLOR)
        stepRect = stepSurf.get_rect()
        stepRect.bottomleft = (20, WINHEIGHT-10)
        DISPLAYSURF.blit(stepSurf, stepRect)
        
        if levelIsComplete:
            solvedRect = IMAGESDICT['solved'].get_rect()
            solvedRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT)
            DISPLAYSURF.blit(IMAGESDICT['solved'], solvedRect)
            if keyPressed:
                return 'solved'
        
        pygame.display.update()
        FPSCLOCK.tick(FPS)
def isLevelFinished(levelObj, gameStateObj):
    for goal in levelObj['goals']:
        if goal not in gameStateObj['stars']:
            return False
    return True
def makeMove(mapObj, gameStateObj, playerMoveTo):
    playerx, playery = gameStateObj['player']
    stars = gameStateObj['stars']
    if playerMoveTo == UP:
        xOffset, yOffset = 0, -1
    elif playerMoveTo == DOWN:
        xOffset, yOffset = 0, 1
    elif playerMoveTo == LEFT:
        xOffset, yOffset = -1, 0
    elif playerMoveTo == RIGHT:
        xOffset, yOffset = 1, 0
    if isWall(mapObj, playerx+xOffset, playery+yOffset):
        return False
    else:
        if (playerx+xOffset, playery+yOffset) in stars:
            if not isBlocked(mapObj, gameStateObj, playerx+xOffset*2, playery+yOffset*2):
                ind = stars.index((playerx+xOffset, playery+yOffset))
                stars[ind] = (stars[ind][0]+xOffset, stars[ind][1]+yOffset)
            else:
                return False
        gameStateObj['player'] = (playerx+xOffset, playery+yOffset)
        return True
def isBlocked(mapObj, gameStateObj, x, y):
    if isWall(mapObj, x, y):
        return True
    elif x<0 or x>=len(mapObj) or y<0 or y>=len(mapObj[x]):
        return True
    elif (x, y) in gameStateObj['stars']:
        return True
    
    return False
    
def terminate():
    pygame.quit()
    sys.exit()
def starpusher():
    global FPSCLOCK, DISPLAYSURF, IMAGESDICT, TILEMAPPING, OUTSIDEDECOMAPPING, BASICFONT, PLAYERIMAGES, currentImage
    pygame.init()
    FPSCLOCK = pygame.time.Clock()
    DISPLAYSURF = pygame.display.set_mode((WINWIDTH, WINHEIGHT))
    pygame.display.set_caption('推星星（推箱子）')
    BASICFONT = pygame.font.Font('res/msyh.ttc', 18)
    IMAGESDICT = {'title':pygame.image.load('res/star_title.png'),
                  'rock': pygame.image.load('res/Rock.png'),
                  'short tree': pygame.image.load('res/Tree_Short.png'),
                  'tall tree': pygame.image.load('res/Tree_Tall.png'),
                  'ugly tree': pygame.image.load('res/Tree_Ugly.png'),
                  'uncovered goal': pygame.image.load('res/RedSelector.png'),
                  'covered goal': pygame.image.load('res/Selector.png'),
                  'star': pygame.image.load('res/Star.png'),
                  'corner': pygame.image.load('res/Wall_Block_Tall.png'),
                  'wall': pygame.image.load('res/Wood_Block_Tall.png'),
                  'inside floor': pygame.image.load('res/Plain_Block.png'),
                  'outside floor': pygame.image.load('res/Grass_Block.png'),
                  'solved': pygame.image.load('res/star_solved.png'),
                  'princess': pygame.image.load('res/princess.png'),
                  'boy': pygame.image.load('res/boy.png'),
                  'catgirl': pygame.image.load('res/catgirl.png'),
                  'horngirl': pygame.image.load('res/horngirl.png'),
                  'pinkgirl': pygame.image.load('res/pinkgirl.png')}
    TILEMAPPING = {'x': IMAGESDICT['corner'],
                   '#': IMAGESDICT['wall'],
                   'o': IMAGESDICT['inside floor'],
                   ' ': IMAGESDICT['outside floor']}
    OUTSIDEDECOMAPPING = {'1':IMAGESDICT['rock'],
                          '2':IMAGESDICT['short tree'],
                          '3':IMAGESDICT['tall tree'],
                          '4':IMAGESDICT['ugly tree']}
    currentImage = 0
    PLAYERIMAGES = [IMAGESDICT['princess'],
                    IMAGESDICT['boy'],
                    IMAGESDICT['catgirl'],
                    IMAGESDICT['horngirl'],
                    IMAGESDICT['pinkgirl']]
    startScreen()
    levels = readLevelsFile('res/StarPusherLevels.txt')
    currentLevelIndex = 0
    while True:
        result = runLevel(levels, currentLevelIndex)
        if result in ('solved', 'next'):
            currentLevelIndex += 1
            currentLevelIndex %= len(levels)
        elif result == 'back':
            currentLevelIndex -= 1
            currentLevelIndex %= len(levels)
        elif result == 'reset':
            pass
if __name__ == "__main__":
    starpusher()